﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.AspNetCore.Razor.Language.Intermediate;

namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version1_X;

public class ViewComponentTagHelperPass : IntermediateNodePassBase, IRazorOptimizationPass
{
    // Run after the default taghelper pass
    public override int Order => IntermediateNodePassBase.DefaultFeatureOrder + 2000;

    protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
    {
        var @namespace = documentNode.FindPrimaryNamespace();
        var @class = documentNode.FindPrimaryClass();
        if (@namespace == null || @class == null)
        {
            // Nothing to do, bail. We can't function without the standard structure.
            return;
        }

        var context = new Context(@namespace, @class);

        // For each VCTH *usage* we need to rewrite the tag helper node to use the tag helper runtime to construct
        // and set properties on the the correct field, and using the name of the type we will generate.
        var nodes = documentNode.FindDescendantNodes<TagHelperIntermediateNode>();
        for (var i = 0; i < nodes.Count; i++)
        {
            var node = nodes[i];
            foreach (var tagHelper in node.TagHelpers)
            {
                RewriteUsage(context, node, tagHelper);
            }
        }

        // Then for each VCTH *definition* that we've seen we need to generate the class that implements
        // ITagHelper and the field that will hold it.
        foreach (var tagHelper in context.TagHelpers)
        {
            AddField(context, tagHelper);
            AddTagHelperClass(context, tagHelper);
        }
    }

    private void RewriteUsage(Context context, TagHelperIntermediateNode node, TagHelperDescriptor tagHelper)
    {
        if (!tagHelper.IsViewComponentKind())
        {
            return;
        }

        context.Add(tagHelper);

        // Now we need to insert a create node using the default tag helper runtime. This is similar to
        // code in DefaultTagHelperOptimizationPass.
        //
        // Find the body node.
        var i = 0;
        while (i < node.Children.Count && node.Children[i] is TagHelperBodyIntermediateNode)
        {
            i++;
        }
        while (i < node.Children.Count && node.Children[i] is DefaultTagHelperBodyIntermediateNode)
        {
            i++;
        }

        // Now find the last create node.
        while (i < node.Children.Count && node.Children[i] is DefaultTagHelperCreateIntermediateNode)
        {
            i++;
        }

        // Now i has the right insertion point.
        node.Children.Insert(i, new DefaultTagHelperCreateIntermediateNode()
        {
            FieldName = context.GetFieldName(tagHelper),
            TagHelper = tagHelper,
            TypeName = context.GetFullyQualifiedName(tagHelper),
        });

        // Now we need to rewrite any set property nodes to use the default runtime.
        for (i = 0; i < node.Children.Count; i++)
        {
            if (node.Children[i] is TagHelperPropertyIntermediateNode propertyNode &&
                propertyNode.TagHelper == tagHelper)
            {
                // This is a set property for this VCTH - we need to replace it with a node
                // that will use our field and property name.
                node.Children[i] = new DefaultTagHelperPropertyIntermediateNode(propertyNode)
                {
                    FieldName = context.GetFieldName(tagHelper),
                    PropertyName = propertyNode.BoundAttribute.GetPropertyName(),
                };
            }
        }
    }

    private void AddField(Context context, TagHelperDescriptor tagHelper)
    {
        // We need to insert a node for the field that will hold the tag helper. We've already generated a field name
        // at this time and use it for all uses of the same tag helper type.
        //
        // We also want to preserve the ordering of the nodes for testability. So insert at the end of any existing
        // field nodes.
        var i = 0;
        while (i < context.Class.Children.Count && context.Class.Children[i] is DefaultTagHelperRuntimeIntermediateNode)
        {
            i++;
        }

        while (i < context.Class.Children.Count && context.Class.Children[i] is FieldDeclarationIntermediateNode)
        {
            i++;
        }

        context.Class.Children.Insert(i, new FieldDeclarationIntermediateNode()
        {
            Annotations =
                {
                    { CommonAnnotations.DefaultTagHelperExtension.TagHelperField, bool.TrueString },
                },
            Modifiers =
                {
                    "private",
                },
            FieldName = context.GetFieldName(tagHelper),
            FieldType = "global::" + context.GetFullyQualifiedName(tagHelper),
        });
    }

    private void AddTagHelperClass(Context context, TagHelperDescriptor tagHelper)
    {
        var node = new ViewComponentTagHelperIntermediateNode()
        {
            ClassName = context.GetClassName(tagHelper),
            TagHelper = tagHelper
        };

        context.Class.Children.Add(node);
    }

    private struct Context
    {
        private readonly Dictionary<TagHelperDescriptor, (string className, string fullyQualifiedName, string fieldName)> _tagHelpers;

        public Context(NamespaceDeclarationIntermediateNode @namespace, ClassDeclarationIntermediateNode @class)
        {
            Namespace = @namespace;
            Class = @class;

            _tagHelpers = new Dictionary<TagHelperDescriptor, (string, string, string)>();
        }

        public ClassDeclarationIntermediateNode Class { get; }

        public NamespaceDeclarationIntermediateNode Namespace { get; }


        public IEnumerable<TagHelperDescriptor> TagHelpers => _tagHelpers.Keys;

        public bool Add(TagHelperDescriptor tagHelper)
        {
            if (_tagHelpers.ContainsKey(tagHelper))
            {
                return false;
            }

            var className = $"__Generated__{tagHelper.GetViewComponentName()}ViewComponentTagHelper";
            var fullyQualifiedName = $"{Namespace.Content}.{Class.ClassName}.{className}";
            var fieldName = GenerateFieldName(tagHelper);

            _tagHelpers.Add(tagHelper, (className, fullyQualifiedName, fieldName));

            return true;
        }

        public string GetClassName(TagHelperDescriptor taghelper)
        {
            return _tagHelpers[taghelper].className;
        }

        public string GetFullyQualifiedName(TagHelperDescriptor taghelper)
        {
            return _tagHelpers[taghelper].fullyQualifiedName;
        }

        public string GetFieldName(TagHelperDescriptor taghelper)
        {
            return _tagHelpers[taghelper].fieldName;
        }

        private static string GenerateFieldName(TagHelperDescriptor tagHelper)
        {
            return $"__{tagHelper.GetViewComponentName()}ViewComponentTagHelper";
        }
    }
}
